컴파일러 처리 과정
소스코드가 CPU에서 실행되기까지의 전체 흐름
전체 흐름
1. 소스코드 : 사람이 작성한 텍스트 코드
2. Lexer : 코드 → 토큰 (의미 단위로 자름)
3. Parser → AST : 토큰 → 구조 트리 (문법적 의미)
4. Bytecode / IR : 트리 → 실행 가능한 저수준 명령어
5. Interpreter/JIT: 바이트코드 실행하거나 기계어로 변환
6. Native 실행 : 최적화된 기계어 코드가 CPU에서 실행
1. 소스코드 (Source Code)
- 사람이 작성한 텍스트. 컴퓨터에게는 단순한 문자열
- 실행하려면 문자열을 구조화된 의미 단위로 해석해야 함
let x = 1 + 2;
2. Lexer (어휘 분석기, Tokenizer)
- 소스코드를 의미 있는 조각(Token) 으로 쪼개는 역할
let x = 1 + 2;
// ↓
['let', 'x', '=', '1', '+', '2', ';']
- 파서는 이 토큰들을 받아야 문법 구조를 만들 수 있음
3. Parser → AST (구문 분석기)
- 토큰들을 조합해서 문법적으로 유효한 구조인지 확인
- AST(Abstract Syntax Tree) 트리 구조로 변환
let x = 1 + 2;
VariableDeclaration ← 변수 선언 전체 (let)
└─ VariableDeclarator ← 변수 하나(x)
├─ Identifier (x) ← 변수 이름
└─ BinaryExpression (+) ← 계산식
├─ Literal (1)
└─ Literal (2)
| 노드 |
역할 |
값 |
VariableDeclaration |
변수 선언 전체 |
kind: "let" |
VariableDeclarator |
변수 하나의 선언과 초기화 |
- |
Identifier |
변수 이름 |
name: "x" |
BinaryExpression |
계산식 |
operator: "+" |
Literal |
숫자 값 |
value: 1, 2 |
다른 예시 - var a = new A.init();
VariableDeclaration (kind: "var")
└── VariableDeclarator
├── id: Identifier (a)
└── init: NewExpression
├── callee: MemberExpression
│ ├── object: Identifier (A)
│ └── property: Identifier (init)
└── arguments: []
4. Bytecode / IR (중간 표현)
- AST는 구조적이지만 실행하기엔 너무 고수준
- 더 실행 친화적인 명령어 집합으로 변환
PUSH 1
PUSH 2
ADD
STORE x
| 구분 |
설명 |
| 바이트코드 |
인터프리터/가상머신 레벨 명령어. JS 엔진이 AST를 매번 재분석하지 않아도 됨 |
| IR |
JIT이나 분석용으로 쓰이는 더 일반화된 형태 |
| 어셈블리어 |
CPU 레벨 명령어 (바이트코드와 다름) |
5. Interpreter / JIT
Interpreter (인터프리터)
- 바이트코드를 한 줄씩 실행
- 예: JavaScriptCore의 LLInt, V8의 Ignition
- 시작은 빠르지만 반복 실행에는 느림
JIT (Just-In-Time 컴파일러)
- 실행 중에 자주 쓰이는 코드를 기계어(Native Code) 로 변환
- 예측(Speculation) 기반으로 최적화
- 예: JavaScriptCore의 DFG JIT / FTL JIT, V8의 TurboFan
- 인터프리터 + JIT 조합으로 최적 성능
6. Native 기계어 실행
- JIT이 만든 코드는 CPU가 바로 실행할 수 있는 기계어
- x86, ARM 등 명령어 집합에 맞게 변환
- JS는 인터프리트 언어지만 JIT을 통해 C/C++ 수준 성능 가능
관련 개념
- 코드 생성 - IR, 중간 코드 생성
- 최적화 - Flat AST, JIT 최적화 기법